import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'
import { type FastifyInstance, fastify } from 'fastify'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { deepClone } from '@/helpers/general'
import { externalValueResolver, loadingStatus, refsEverywhere, restoreOriginalRefs } from '@/plugins/bundler'

describe('plugins', () => {
  describe('loadingStatus', () => {
    it('sets the loading status to the correct value during ref resolution', async () => {
      const createDeferred = <T>() => {
        let resolve!: (value: T) => void
        const promise = new Promise<T>((res) => (resolve = res))
        return { promise, resolve }
      }

      const started = createDeferred<void>()
      const succeeded = createDeferred<void>()

      const onResolveStart = vi.fn(() => started.resolve())
      const onResolveSuccess = vi.fn(() => succeeded.resolve())
      const onResolveError = vi.fn()

      const input = {
        a: {
          b: 'hello',
        },
        c: {
          $ref: 'some-ref',
        },
      } as any

      void bundle(input, {
        plugins: [
          {
            type: 'loader',
            validate: (value: string) => value === 'some-ref',
            exec: () =>
              Promise.resolve({
                ok: true,
                data: { resolved: true },
                raw: JSON.stringify({ message: 'Resolved document' }),
              }),
          },
          {
            type: 'lifecycle',
            onResolveStart,
            onResolveError,
            onResolveSuccess,
          },
          loadingStatus(),
        ],
        treeShake: false,
      })

      await started.promise

      // Verify that the loading status was set during resolution
      expect(onResolveStart).toHaveBeenCalled()
      expect(input.c['$status']).toBe('loading')

      await succeeded.promise

      // Verify that the loading status was set during resolution
      expect(onResolveSuccess).toHaveBeenCalled()
      expect(input.c['$status']).toBeUndefined() // Should be removed after successful resolution

      expect(onResolveError).not.toHaveBeenCalled()
    })

    it('sets error status when ref resolution fails', async () => {
      const input = {
        c: {
          $ref: 'invalid-ref',
        },
      } as any

      const onResolveError = vi.fn()

      void bundle(input, {
        plugins: [
          {
            type: 'loader',
            validate: (value: string) => value === 'invalid-ref',
            exec: () =>
              Promise.resolve({
                ok: false,
                error: 'Failed to resolve reference',
              }),
          },
          {
            type: 'lifecycle',
            onResolveError,
          },
          loadingStatus(),
        ],
        treeShake: false,
      })

      await vi.waitFor(() => expect(onResolveError).toHaveBeenCalled())

      // Verify that the error status was set
      expect(input.c.$status).toBe('error')
    })
  })

  describe('externalValueResolver', () => {
    let server: FastifyInstance
    const port = 9809
    const url = `http://localhost:${port}`

    beforeEach(() => {
      server = fastify({ logger: false })

      return async () => {
        await server.close()
      }
    })

    it('resolves external values', async () => {
      server.get('/', () => {
        return { a: 'a' }
      })

      await server.listen({ port })

      const input = {
        hello: 'world',
        a: {
          externalValue: url,
        },
      }

      await bundle(input, {
        treeShake: false,
        plugins: [externalValueResolver(), fetchUrls()],
      })

      expect(input).toEqual({
        hello: 'world',
        a: {
          externalValue: url,
          value: { a: 'a' },
        },
      })
    })
  })

  describe('refsEverywhere', () => {
    let server: FastifyInstance
    const port = 9181
    const url = `http://localhost:${port}`

    beforeEach(() => {
      server = fastify({ logger: false })

      return async () => {
        await server.close()
      }
    })

    it('inline refs on the info object', async () => {
      server.get('/', () => {
        return {
          description: 'Some description',
        }
      })
      await server.listen({ port })

      const input = {
        info: {
          $ref: url,
        },
      }

      const result = await bundle(input, {
        treeShake: false,
        plugins: [refsEverywhere(), fetchUrls()],
      })

      expect(result).toEqual({
        info: {
          description: 'Some description',
        },
      })
    })

    it('does not inline refs if the info object is not top level', async () => {
      server.get('/', () => {
        return {
          description: 'Some description',
        }
      })
      await server.listen({ port })

      const input = {
        someProp: {
          info: {
            $ref: url,
          },
        },
      }

      const result = await bundle(input, {
        treeShake: false,
        plugins: [fetchUrls(), refsEverywhere()],
      })

      expect(result).toEqual({
        someProp: {
          info: {
            '$ref': '#/x-ext/44fe49b',
          },
        },
        'x-ext': {
          '44fe49b': {
            description: 'Some description',
          },
        },
      })
    })
  })

  describe('restoreOriginalRefs', () => {
    let server: FastifyInstance
    const port = 9088
    const url = `http://localhost:${port}`

    beforeEach(() => {
      server = fastify({ logger: false })

      return async () => {
        await server.close()
      }
    })

    it('restores the original references', { timeout: 100000 }, async () => {
      server.get('/', () => ({ description: 'Some resolved value' }))
      await server.listen({ port })

      const originalInput = {
        a: 'a',
        b: {
          $ref: url,
        },
      }

      const input = deepClone(originalInput)

      await bundle(input, {
        plugins: [fetchUrls()],
        treeShake: false,
        urlMap: true,
      })

      await bundle(input, {
        treeShake: false,
        urlMap: true,
        plugins: [restoreOriginalRefs()],
      })

      expect(input).toEqual({
        ...originalInput,
        'x-ext': {
          'ad870b6': {
            'description': 'Some resolved value',
          },
        },
        'x-ext-urls': {
          'ad870b6': 'http://localhost:9088',
        },
      })
    })
  })
})
